﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;

namespace DependencyAnalysis.DataModel
{
    [Serializable]
    [DataContract]
    [KnownType(typeof(MemberDescriptor))]
    [KnownType(typeof(TypeDescriptor))]
    [KnownType(typeof(SubmoduleDescriptor))]
    [KnownType(typeof(ModuleDescriptor))]
    [KnownType(typeof(DependencyDescriptor))]
    [KnownType(typeof(HashSet<DependencyDescriptor>))]
    [KnownType(typeof(InstructionCache))]
    public class DependencyModel : IDependencyModel
    {
        private readonly object _lockMembers = new object();
        private readonly object _lockTypes = new object();
        private readonly object _lockSubmodules = new object();
        private readonly object _lockModules = new object();

        [DataMember(Order = 1)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerMember = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();
        [DataMember(Order = 2)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerType = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();
        [DataMember(Order = 3)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerSubmodule = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();
        [DataMember(Order = 4)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerModule = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();

        [DataMember(Order = 5)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerMemberOut = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();
        [DataMember(Order = 6)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerTypeOut = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();
        [DataMember(Order = 7)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerSubmoduleOut = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();
        [DataMember(Order = 8)]
        private readonly Dictionary<IDescriptor, ISet<DependencyDescriptor>> _dependenciesPerModuleOut = new Dictionary<IDescriptor, ISet<DependencyDescriptor>>();

        [DataMember(Order = 9)]
        private readonly InstructionCache _instructionCache = new InstructionCache();

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerType
        {
            get { return _dependenciesPerType; }
        }

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerSubmodule
        {
            get { return _dependenciesPerSubmodule; }
        }

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerModule
        {
            get { return _dependenciesPerModule; }
        }

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerMember
        {
            get { return _dependenciesPerMember; }
        }

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerTypeOut
        {
            get { return _dependenciesPerTypeOut; }
        }

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerSubmoduleOut
        {
            get { return _dependenciesPerSubmoduleOut; }
        }

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerModuleOut
        {
            get { return _dependenciesPerModuleOut; }
        }

        public IDictionary<IDescriptor, ISet<DependencyDescriptor>> DependenciesPerMemberOut
        {
            get { return _dependenciesPerMemberOut; }
        }

        public InstructionCache InstructionCache
        {
            get
            {
                if (_instructionCache == null)
                {
                    throw new InvalidOperationException("Instructions have not been serialized. Run DependencyAnalysis in standalone mode.");
                }

                return _instructionCache;
            }
        }

        private ProjectDescriptor _project;
        public ProjectDescriptor Project
        {
            get
            {
                lock (_lockModules)
                {
                    if (_project == null)
                    {
                        string identifier = "<generic+project>" + Guid.NewGuid();
                        var modules = from x in _dependenciesPerModuleOut.Keys
                                      where ((ModuleDescriptor)x).IsPartOfProject
                                      select (ModuleDescriptor)x;
                        _project = new ProjectDescriptor
                        {
                            Identifier = identifier,
                            FullName = identifier,
                            Modules = modules
                        };
                    }
                    return _project;
                }
            }
        }

        public void Clear()
        {
            lock (_lockMembers)
            {
                lock (_lockTypes)
                {
                    lock (_lockSubmodules)
                    {
                        lock (_lockModules)
                        {
                            _dependenciesPerMember.Clear();
                            _dependenciesPerType.Clear();
                            _dependenciesPerType.Clear();
                            _dependenciesPerModule.Clear();

                            _dependenciesPerMemberOut.Clear();
                            _dependenciesPerTypeOut.Clear();
                            _dependenciesPerTypeOut.Clear();
                            _dependenciesPerModuleOut.Clear();

                            _project = null;
                        }
                    }
                }
            }
        }

        public void Add<T>(T descriptor) where T : IDescriptor
        {
            var dependency = descriptor as DependencyDescriptor;
            if (dependency != null)
            {
                AddDependency(dependency);
                return;
            }
            var dependencyStructure = DependencyModelConverter.GetDependencyStructureIn<T>(this);
            var dependencyStructureOut = DependencyModelConverter.GetDependencyStructureOut<T>(this);
            if (dependencyStructure == null ||
                dependencyStructureOut == null) return;
            lock (GetLock<T>())
            {
                if (!dependencyStructure.ContainsKey(descriptor))
                    dependencyStructure.Add(descriptor, new HashSet<DependencyDescriptor>());
                if (!dependencyStructureOut.ContainsKey(descriptor))
                    dependencyStructureOut.Add(descriptor, new HashSet<DependencyDescriptor>());
            }
        }

        private void AddDependency(DependencyDescriptor dependency)
        {
            var targetMember = dependency.Target;
            var sourceMember = dependency.Source;
            if (sourceMember.Equals(targetMember)) return;
            lock (_lockMembers)
            {
                if (!AddDependency(dependency, _dependenciesPerMember, targetMember)) return;
                AddDependency(dependency, _dependenciesPerMemberOut, sourceMember);
                AddDescriptorToDependencyStructure(_dependenciesPerMember, sourceMember);
                AddDescriptorToDependencyStructure(_dependenciesPerMemberOut, targetMember);
            }
            var targetType = targetMember.ImplementingType;
            var sourceType = sourceMember.ImplementingType;
            if (sourceType.Equals(targetType)) return;
            lock (_lockTypes)
            {
                if (!AddDependency(dependency, _dependenciesPerType, targetType)) return;
                AddDependency(dependency, _dependenciesPerTypeOut, sourceType);
                AddDescriptorToDependencyStructure(_dependenciesPerType, sourceType);
                AddDescriptorToDependencyStructure(_dependenciesPerTypeOut, targetType);
            }
            var targetNamespace = targetType.Submodule;
            var sourceNamespace = sourceType.Submodule;
            if (sourceNamespace.Equals(targetNamespace)) return;
            lock (_lockSubmodules)
            {
                if (!AddDependency(dependency, _dependenciesPerSubmodule, targetNamespace)) return;
                AddDependency(dependency, _dependenciesPerSubmoduleOut, sourceNamespace);
                AddDescriptorToDependencyStructure(_dependenciesPerSubmodule, sourceNamespace);
                AddDescriptorToDependencyStructure(_dependenciesPerSubmoduleOut, targetNamespace);
            }
            var targetModule = targetNamespace.Module;
            var sourceModule = sourceNamespace.Module;
            if (sourceModule.Equals(targetModule)) return;
            lock (_lockModules)
            {
                AddDependency(dependency, _dependenciesPerModule, targetModule);
                AddDependency(dependency, _dependenciesPerModuleOut, sourceModule);
                AddDescriptorToDependencyStructure(_dependenciesPerModule, sourceModule);
                AddDescriptorToDependencyStructure(_dependenciesPerModuleOut, targetModule);
            }
        }

        private static bool AddDependency<TDescriptor>(DependencyDescriptor dependency, Dictionary<TDescriptor, ISet<DependencyDescriptor>> dependencyMapping,
            TDescriptor target) where TDescriptor : IDescriptor
        {
            if (target == null || string.IsNullOrEmpty(target.FullName))
            {
                return false;
            }
            ISet<DependencyDescriptor> dependencyDescriptors = AddDescriptorToDependencyStructure(dependencyMapping, target);
            if (!dependencyDescriptors.Contains(dependency))
            {
                dependencyDescriptors.Add(dependency);
                return true;
            }
            return false;
        }

        private static ISet<DependencyDescriptor> AddDescriptorToDependencyStructure<TDescriptor>(Dictionary<TDescriptor, ISet<DependencyDescriptor>> dependencyMapping, TDescriptor target)
            where TDescriptor : IDescriptor
        {
            ISet<DependencyDescriptor> dependencyDescriptors;
            if (dependencyMapping.ContainsKey(target))
            {
                dependencyDescriptors = dependencyMapping[target];
            }
            else
            {
                dependencyDescriptors = new HashSet<DependencyDescriptor>();
                dependencyMapping[target] = dependencyDescriptors;
            }
            return dependencyDescriptors;
        }

        private object GetLock<T>()
        {
            if (typeof(T) == typeof(MemberDescriptor))
            {
                return _lockMembers;
            }
            if (typeof(T) == typeof(TypeDescriptor))
            {
                return _lockTypes;
            }
            if (typeof(T) == typeof(SubmoduleDescriptor))
            {
                return _lockSubmodules;
            }
            if (typeof(T) == typeof(ModuleDescriptor))
            {
                return _lockModules;
            }
            throw new ArgumentException();
        }
    }
}
